﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using WF_AppFuel.Base.Exceptions;
using WF_AppFuel.Validation.Attributes;
using WF_AppFuel.Validation.Base;
using System.Reflection;
using WF_AppFuel.Validation.Engines;

namespace WF_AppFuel.Validation
{
    /// <summary>
    /// The Validator object is used for validating objects marked with
    /// the various validation attributes.
    /// </summary>
    /// <author>Arjay C. Nacion</author>
    public sealed class Validator : IDisposable
    {
        private bool isDisposed;
        private IDictionary<Type, BaseValidationEngine> enginesDictionary;
        private IList<object> subjects;
        private IList<string> validationMessages;

        public Validator()
        {
            enginesDictionary = new Dictionary<Type, BaseValidationEngine>();
            subjects = new List<object>();
            validationMessages = new List<string>();
            this.InitializeValidationEngines();
        }

        private void InitializeValidationEngines()
        {
            enginesDictionary.Add(typeof(IsRequiredValidationEngine), 
                new IsRequiredValidationEngine());
            enginesDictionary.Add(typeof(IsURLValidationEngine), 
                new IsURLValidationEngine());
            enginesDictionary.Add(typeof(IsLessThanValidationEngine), 
                new IsLessThanValidationEngine());
            enginesDictionary.Add(typeof(IsGreaterThanValidationEngine), 
                new IsGreaterThanValidationEngine());
            enginesDictionary.Add(typeof(IsEmailValidationEngine),
                new IsEmailValidationEngine());
            enginesDictionary.Add(typeof(IsAlphaValidationEngine),
                new IsAlphaValidationEngine());
            enginesDictionary.Add(typeof(IsAlphaNumericValidationEngine),
                new IsAlphaNumericValidationEngine());
        }

        /// <summary>
        /// Adds a new object subject for validation.
        /// </summary>
        public void AddSubject(object subject)
        {
            if (subject == null)
                throw new InvalidArgumentException("Validation subject cannot be null.");

            if (!HasRequiresValidationAttribute(subject))
                throw new InvalidArgumentException("Subject must be marked with the " +
                    "RequiresValidation attribute");

            if (!subjects.Contains(subject))
                subjects.Add(subject);
        }

        /// <summary>
        /// Performs validation on all subjects.
        /// </summary>
        public void Validate()
        {
            validationMessages.Clear();
            foreach (object subject in subjects)
            {
                Type subjectType = subject.GetType();
                PropertyInfo[] props = subjectType.GetProperties();

                foreach (PropertyInfo prop in props)
                {
                    object[] propAttribs = prop.GetCustomAttributes(
                        typeof(IValidationAttribute), true);
                    string[] messages = DoValidate(subject, prop, propAttribs);

                    if (messages.Count() > 0)
                    {
                        foreach (string message in messages)
                        {
                            validationMessages.Add(message + " for object of type [" +
                                subjectType.Name + "]");
                        }
                    }
                }
            }
        }

        /// <summary>
        /// The list of validation messages which the instance of
        /// this Validator stored after performing validation on all subjects.
        /// </summary>
        public IList<string> ValidationMessages
        {
            get { return this.validationMessages; }
        }

        private string[] DoValidate(object subject, PropertyInfo prop, object[] propAttrbs)
        {
            IList<string> messages = new List<string>();
            string propertyName = prop.Name;
            object propertyValue = prop.GetValue(subject, null);

            //check if propAttribs contains the Required validation attribute
            var requiredAttrib = from pa in propAttrbs
                                 where pa.GetType().Name.Equals("IsRequired")
                                 select pa;
            var otherValidationAttribs = from pa in propAttrbs
                                         where !pa.GetType().Name.Equals("IsRequired")
                                         select pa;
            //Validate required fields
            if (requiredAttrib != null && requiredAttrib.Count() > 0)
            {
                IValidationAttribute validationAttrib = requiredAttrib.ElementAt(0) as IValidationAttribute;
                IsRequired isRequired = (IsRequired)validationAttrib;

                BaseValidationEngine validationEngine = enginesDictionary[isRequired.ValidationEngineType];
                validationEngine.Value = propertyValue;
                validationEngine.ValidationAttribute = isRequired;

                if (!validationEngine.Run())
                    messages.Add(propertyName + " " + isRequired.ValidationMessage);
            }
            else
            {
                /*
                 * If the property is not marked with IsRequired attribute, but contains other
                 * validation attributes, check if the value is not null then validate 
                 * as annotated by the various validation attributes of the property.
                 */
                if (propertyValue != null)
                {
                    foreach (var otherValidationAttrib in otherValidationAttribs)
                    {
                        IValidationAttribute validationAttrib = (IValidationAttribute)otherValidationAttrib;
                        BaseValidationEngine validationEngine = 
                            enginesDictionary[validationAttrib.ValidationEngineType];
                        validationEngine.ValidationAttribute = validationAttrib;
                        validationEngine.Value = propertyValue;

                        if (!validationEngine.Run())
                            messages.Add(propertyName + " " + validationAttrib.ValidationMessage);
                    }
                }
            }

            return messages.ToArray();
        }

        /*
         * Checks if the subject contains the RequiresValidation attribute
         */
        private static bool HasRequiresValidationAttribute(object subject)
        {
            Type type = subject.GetType();
            object[] attributes = type.GetCustomAttributes(false);

            var attrib = from a in attributes
                         where a.GetType().Equals(typeof(RequiresValidation))
                         select a;
            
            return attrib != null && attrib.Count() > 0;
        }

        public void Dispose()
        {
            if (!this.isDisposed)
            {
                if (enginesDictionary != null)
                {
                    enginesDictionary.Clear();
                    enginesDictionary = null;
                }

                if (subjects != null)
                {
                    subjects.Clear();
                    subjects = null;
                }
                this.isDisposed = true;
            }
        }

    }
}
